// SPDX-License-Identifier: GPL-2.0+
/*
 * KMS driver for Loongson display controller
 * Copyright (C) 2022 Loongson Corporation
 */

/*
 * Authors:
 *      Sui Jingfeng <suijingfeng@loongson.cn>
 */

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/console.h>

#include <drm/drm_print.h>
#include <drm/drm_drv.h>
#include <drm/drm_atomic_helper.h>

#include "lsdc_drv.h"
#include "lsdc_regs.h"

static const struct lsdc_chip_desc dc_in_ls7a1000 = {
	.chip = LSDC_CHIP_7A1000,
	.num_of_crtc = LSDC_NUM_CRTC,
	.max_pixel_clk = 200000,
	.max_width = 2048,
	.max_height = 2048,
	.num_of_hw_cursor = 1,
	.hw_cursor_w = 32,
	.hw_cursor_h = 32,
	.stride_alignment = 256,
	.has_builtin_i2c = true,
	.has_vram = true,
	.broken_gamma = true,
};

static const struct lsdc_chip_desc dc_in_ls7a2000 = {
	.chip = LSDC_CHIP_7A2000,
	.num_of_crtc = LSDC_NUM_CRTC,
	.max_pixel_clk = 200000,
	.max_width = 2048,
	.max_height = 2048,
	.num_of_hw_cursor = 2,
	.hw_cursor_w = 64,
	.hw_cursor_h = 64,
	.stride_alignment = 256,
	.has_builtin_i2c = true,
	.has_vram = true,
	.broken_gamma = true,
};

static int lsdc_drm_suspend(struct drm_device *ddev, bool suspend)
{
	drm_mode_config_helper_suspend(ddev);

	if (suspend && ddev->pdev) {
		pci_save_state(ddev->pdev);
			pci_set_power_state(ddev->pdev, PCI_D3hot);
			/* Shut down the device */
			pci_disable_device(ddev->pdev);
	}

	return 0;
}

static int lsdc_drm_resume(struct drm_device *ddev, bool resume)
{
	if (resume && ddev->pdev) {
		pci_set_power_state(ddev->pdev, PCI_D0);
		pci_restore_state(ddev->pdev);

		if (pci_enable_device(ddev->pdev))
			return -EIO;
	}

	drm_mode_config_helper_resume(ddev);

	return 0;
	}

static int lsdc_drm_pm_suspend(struct device *dev)
{
	struct lsdc_device *ldev = dev_get_drvdata(dev);

	return lsdc_drm_suspend(ldev->ddev, true);
}

static int lsdc_drm_pm_resume(struct device *dev)
{
	struct lsdc_device *ldev = dev_get_drvdata(dev);

	return lsdc_drm_resume(ldev->ddev, true);
	}

static int lsdc_drm_pm_freeze(struct device *dev)
{
	struct lsdc_device *ldev = dev_get_drvdata(dev);

	return lsdc_drm_suspend(ldev->ddev, false);
}

static int lsdc_drm_pm_thaw(struct device *dev)
{
	struct lsdc_device *ldev = dev_get_drvdata(dev);

	return lsdc_drm_resume(ldev->ddev, false);
}

static int lsdc_vram_init(struct lsdc_device *ldev)
{
	const struct lsdc_chip_desc * const descp = ldev->desc;
	struct pci_dev *gpu;
	resource_size_t base, size;

	if (descp->chip == LSDC_CHIP_7A2000) {
		/* BAR 2 of LS7A2000's GPU contain VRAM */
		gpu = pci_get_device(PCI_VENDOR_ID_LOONGSON, 0x7A25, NULL);
	} else if (descp->chip == LSDC_CHIP_7A1000) {
		/* BAR 2 of LS7A1000's GPU(GC1000) contain VRAM */
		gpu = pci_get_device(PCI_VENDOR_ID_LOONGSON, 0x7A15, NULL);
	} else {
		dev_err(ldev->dev, "Unknown chip, the driver need update\n");
		return -ENOENT;
	}

	if (IS_ERR_OR_NULL(gpu)) {
		dev_err(ldev->dev, "Can not get VRAM\n");
		return -ENOENT;
	}

	base = pci_resource_start(gpu, 2);
	size = pci_resource_len(gpu, 2);

	ldev->vram_base = base;
	ldev->vram_size = size;

	dev_info(ldev->dev, "vram start: 0x%llx, size: %uMB\n",
		 (u64)base, (u32)(size >> 20));

	return 0;
}

static int lsdc_pci_probe(struct pci_dev *pdev,
			  const struct pci_device_id * const ent)
{
	struct device *dev = &pdev->dev;
	struct lsdc_device *ldev;
	const struct lsdc_chip_desc *desc;
	struct resource *bar0;
	int ret;
	u8 chip_revision;

	ret = pcim_enable_device(pdev);
	if (ret) {
		DRM_ERROR("Enable pci devive failed.\n");
		return ret;
	}

	pci_set_master(pdev);

	ldev = kzalloc(sizeof(struct lsdc_device), GFP_KERNEL);
	if (ldev == NULL)
		return -ENOMEM;

	desc = (struct lsdc_chip_desc *)ent->driver_data;

	ldev->desc = desc;
	ldev->dev = dev;

	if (desc == NULL) {
		DRM_ERROR("unknown dc chip core\n");
		ret = -ENOENT;
		goto err_free;
	}

	ldev->irq = pdev->irq;

	/* BAR 0 contains registers */
	bar0 = devm_request_mem_region(dev,
				       pci_resource_start(pdev, 0),
				       pci_resource_len(pdev, 0),
				       dev_name(dev));

	if (bar0 == NULL) {
		DRM_ERROR("Can't reserve mmio registers\n");
		ret = -ENOMEM;
		goto err_free;
	}

	ldev->reg_base = pci_iomap_range(pdev, 0, 0, 0);
	if (ldev->reg_base == NULL) {
		DRM_ERROR("Unable to get lsdc registers\n");
		ret = -ENOMEM;
		goto err_free;
	}

	ret = lsdc_vram_init(ldev);
	if (ret)
		goto err_free;

	/* Get the revison of this DC */
	pci_read_config_byte(pdev, 0x8, &chip_revision);
	DRM_INFO("DC revision is %d\n", chip_revision);

	if (!request_mem_region(ldev->vram_base, ldev->vram_size,
				"lsdc_drmfb_vram")) {
		DRM_ERROR("can't reserve VRAM\n");
		ret = -ENXIO;
		goto err_free;
	}

	DRM_INFO("vram start: 0x%llx, size: %lluMB\n",
		 ldev->vram_base, ldev->vram_size >> 20);

	ldev->vram = devm_ioremap_wc(dev, ldev->vram_base, ldev->vram_size);
	if (ldev->vram == NULL) {
		ret = -ENOMEM;
		goto err_free;
	}

	DRM_INFO("vram virtual addr: 0x%llx\n", (u64)ldev->vram);

	ldev->ddev->pdev = pdev;

	dev_set_drvdata(dev, ldev);

	ret = lsdc_common_probe(ldev);
	if (ret)
		goto err_free;

	return ret;

err_free:
	kfree(ldev);

	return ret;
}

static void lsdc_pci_remove(struct pci_dev *pdev)
{
	struct lsdc_device *ldev = dev_get_drvdata(&pdev->dev);

	if (ldev && ldev->vram) {
		devm_iounmap(&pdev->dev, ldev->vram);
		ldev->vram = NULL;
	}

	lsdc_common_remove(ldev);
	pci_clear_master(pdev);
	pci_release_regions(pdev);
}

const struct dev_pm_ops lsdc_drm_pm_ops = {
	.suspend = lsdc_drm_pm_suspend,
	.resume = lsdc_drm_pm_resume,
	.freeze = lsdc_drm_pm_freeze,
	.thaw = lsdc_drm_pm_thaw,
	.poweroff = lsdc_drm_pm_freeze,
	.restore = lsdc_drm_pm_resume,
};

static const struct pci_device_id lsdc_pciid_list[] = {
	{PCI_VENDOR_ID_LOONGSON, 0x7a06, PCI_ANY_ID, PCI_ANY_ID, 0, 0, (kernel_ulong_t)&dc_in_ls7a1000},
	{PCI_VENDOR_ID_LOONGSON, 0x7a36, PCI_ANY_ID, PCI_ANY_ID, 0, 0, (kernel_ulong_t)&dc_in_ls7a2000},
	{0, 0, 0, 0, 0, 0, 0}
};

struct pci_driver lsdc_pci_driver = {
	.name = DRIVER_NAME,
	.id_table = lsdc_pciid_list,
	.probe = lsdc_pci_probe,
	.remove = lsdc_pci_remove,
	.driver.pm = &lsdc_drm_pm_ops,
};

MODULE_DEVICE_TABLE(pci, lsdc_pciid_list);

static int __init lsdc_pci_init(void)
{
	struct pci_dev *pdev = NULL;

	/* Multi video card workaround */
	while ((pdev = pci_get_class(PCI_CLASS_DISPLAY_VGA << 8, pdev))) {
		if (pdev->vendor != PCI_VENDOR_ID_LOONGSON) {
			DRM_INFO("Discrete graphic card detected, abort.\n");
			return 0;
		}
	}

	if (vgacon_text_force()) {
		DRM_ERROR("VGACON disables loongson kernel modesetting.\n");
		return -EINVAL;
	}

	return pci_register_driver(&lsdc_pci_driver);
}
module_init(lsdc_pci_init);

static void __exit lsdc_pci_exit(void)
{
	return pci_unregister_driver(&lsdc_pci_driver);
}
module_exit(lsdc_pci_exit);

MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_LICENSE("GPL v2");